package glapp;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import javax.imageio.*;

import ui.Message;

/**
 * Loads an image from file, stores pixels as ARGB int array, and RGBA
 * ByteBuffer for use in OpenGL. Can convert images to power-of-2 dimensions for
 * textures.
 * <P>
 * Static functions are included to load, flip and convert pixel arrays.
 * <P>
 * napier at potatoland dot org
 */

class GLImage
{
	protected int			h			= 0;
	protected int			w			= 0;
	protected ByteBuffer	pixelBuffer	= null; // store pixels as bytes in GL_RGBA format
	private int[]		pixels		= null; // store pixels as ARGB integers

	private int			textureW;			// the power-of-two dimensions that can hold this image (ie. an image 250x200 would have textureSize 256)
	private int			textureH;

	public GLImage()
	{
	}

	/**
	 * Load pixels from an image file. Flip Y axis. Convert to RGBA format.
	 * 
	 * @param imgName
	 */
	protected GLImage(String imgName)
	{
		BufferedImage img = loadJavaImage(imgName);
		if (makeGLImage(img, true, false))
		{
			Message.getInstance().msg("GLImage(String): loaded " + imgName + ", width=" + w + " height=" + h);
		}
	}

	/**
	 * Make a BufferedImage from the contents of an image file.
	 * 
	 * @param imageFileContents
	 *            byte array containing the guts of a JPG, GIF, or PNG
	 */
	private BufferedImage makeBufferedImage(byte[] imageFileContents)
	{
		BufferedImage bi = null;
		try
		{
			InputStream in = new ByteArrayInputStream(imageFileContents);
			bi = javax.imageio.ImageIO.read(in);
		}
		catch (IOException ioe)
		{
			Message.getInstance().err("GLImage.makeBufferedImage(): " + ioe);
		}
		return bi;
	}

	/**
	 * return true if image has been loaded successfully
	 * 
	 * @return
	 */
	public boolean isLoaded()
	{
		return (pixelBuffer != null);
	}


	/**
	 * Load an image from the given filename. If convertToPow2 is true then
	 * convert the image to a power of two. Store pixels as ARGB ints in the
	 * pixels array and as RGBA bytes in the pixelBuffer ByteBuffer. Hold onto
	 * image width/height.
	 * 
	 * @param imgName
	 */
	private boolean makeGLImage(BufferedImage tmpi, boolean flipYaxis, boolean convertToPow2)
	{
		if (tmpi != null)
		{
			if (flipYaxis)
			{
				tmpi = flipY(tmpi);
			}
			if (convertToPow2)
			{
				tmpi = convertToPowerOf2(tmpi);
			}
			w = tmpi.getWidth(null);
			h = tmpi.getHeight(null);
			pixels = getImagePixels(tmpi); // pixels in default Java ARGB format
			pixelBuffer = convertImagePixelsRGBA(pixels, w, h, false); // convert to bytes in RGBA format
			textureW = GLApp.getPowerOfTwoBiggerThan(w); // the texture size big enough to hold this image
			textureH = GLApp.getPowerOfTwoBiggerThan(h); // the texture size big enough to hold this image
			//GLApp.msg("GLImage: loaded " + imgName + ", width=" + w + " height=" + h);
			return true;
		}
		else
		{
			//GLApp.err("GLImage: FAILED TO LOAD IMAGE " + imgName);
			pixels = null;
			pixelBuffer = null;
			h = w = 0;
			return false;
		}
	}

	/**
	 * Load a BufferedImage from the given image file name. File can be in the
	 * local filesytem, in the applet folder, or in a jar.
	 */
	public BufferedImage loadJavaImage(String imgName)
	{
		BufferedImage tmpi = null;
		try
		{
			tmpi = ImageIO.read(GLApp.getInputStream(imgName));
		}
		catch (Exception e)
		{
			Message.getInstance().err("GLImage.loadJavaImage() exception: FAILED TO LOAD IMAGE " + e);
		}
		return tmpi;
	}

	/**
	 * Return the Image pixels in default Java int ARGB format.
	 * 
	 * @return
	 */
	public static int[] getImagePixels(Image image)
	{
		int[] pixelsARGB = null;
		if (image != null)
		{
			int imgw = image.getWidth(null);
			int imgh = image.getHeight(null);
			pixelsARGB = new int[imgw * imgh];
			PixelGrabber pg = new PixelGrabber(image, 0, 0, imgw, imgh, pixelsARGB, 0, imgw);
			try
			{
				pg.grabPixels();
			}
			catch (Exception e)
			{
				Message.getInstance().err("Pixel Grabbing interrupted!");
				return null;
			}
		}
		return pixelsARGB;
	}

	/**
	 * return int array containing pixels in ARGB format (default Java byte
	 * order).
	 */
	public int[] getPixelInts()
	{
		return pixels;
	}

	/**
	 * return ByteBuffer containing pixels in RGBA format (commmonly used in
	 * OpenGL).
	 */
	public ByteBuffer getPixelBytes()
	{
		return pixelBuffer;
	}

	//========================================================================
	//
	// Static convertion functions to prepare pixels for use in OpenGL
	//
	//========================================================================

	/**
	 * Flip an array of pixels vertically
	 * 
	 * @param imgPixels
	 * @param imgw
	 * @param imgh
	 * @return int[]
	 */
	public static int[] flipPixels(int[] imgPixels, int imgw, int imgh)
	{
		int[] flippedPixels = null;
		if (imgPixels != null)
		{
			flippedPixels = new int[imgw * imgh];
			for (int y = 0; y < imgh; y++)
			{
				for (int x = 0; x < imgw; x++)
				{
					flippedPixels[((imgh - y - 1) * imgw) + x] = imgPixels[(y * imgw) + x];
				}
			}
		}
		return flippedPixels;
	}

	/**
	 * Convert ARGB pixels to a ByteBuffer containing RGBA pixels. The GL_RGBA
	 * format is a default format used in OpenGL 1.0, but requires that we move
	 * the Alpha byte for each pixel in the image (slow). Would be better to use
	 * OpenGL 1.2 GL_BGRA format and leave pixels in the ARGB format (faster)
	 * but this pixel format caused problems when creating mipmaps (see note
	 * above). .
	 * <P>
	 * If flipVertically is true, pixels will be flipped vertically (for OpenGL
	 * coord system).
	 * 
	 * @return ByteBuffer
	 */
	public static ByteBuffer convertImagePixelsRGBA(int[] jpixels, int imgw, int imgh, boolean flipVertically)
	{
		byte[] bytes; // will hold pixels as RGBA bytes
		if (flipVertically)
		{
			jpixels = flipPixels(jpixels, imgw, imgh); // flip Y axis
		}
		bytes = convertARGBtoRGBA(jpixels);
		return allocBytes(bytes); // convert to ByteBuffer and return
	}

	/**
	 * Convert pixels from java default ARGB int format to byte array in RGBA
	 * format.
	 * 
	 * @param jpixels
	 * @return
	 */
	public static byte[] convertARGBtoRGBA(int[] jpixels)
	{
		byte[] bytes = new byte[jpixels.length * 4]; // will hold pixels as RGBA bytes
		int p, r, g, b, a;
		int j = 0;
		for (int i = 0; i < jpixels.length; i++)
		{
			p = jpixels[i];
			a = (p >> 24) & 0xFF; // get pixel bytes in ARGB order
			r = (p >> 16) & 0xFF;
			g = (p >> 8) & 0xFF;
			b = (p >> 0) & 0xFF;
			bytes[j + 0] = (byte) r; // fill in bytes in RGBA order
			bytes[j + 1] = (byte) g;
			bytes[j + 2] = (byte) b;
			bytes[j + 3] = (byte) a;
			j += 4;
		}
		return bytes;
	}

	//========================================================================
	// Utility functions
	//========================================================================

	/**
	 * Same function as in GLApp.java. Allocates a ByteBuffer to hold the given
	 * array of bytes.
	 * 
	 * @param bytearray
	 * @return ByteBuffer containing the contents of the byte array
	 */
	public static ByteBuffer allocBytes(byte[] bytearray)
	{
		ByteBuffer bb = ByteBuffer.allocateDirect(bytearray.length).order(ByteOrder.nativeOrder());
		bb.put(bytearray).flip();
		return bb;
	}

	/**
	 * Scale this GLImage so width and height are powers of 2. Recreate pixels
	 * and pixelBuffer.
	 */
	public void convertToPowerOf2()
	{
		// make BufferedImage from original pixels
		BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
		image.setRGB(0, 0, w, h, pixels, 0, w);

		// scale into new image
		BufferedImage scaledImg = convertToPowerOf2(image);

		// resample pixel data
		w = scaledImg.getWidth(null);
		h = scaledImg.getHeight(null);
		pixels = getImagePixels(scaledImg); // pixels in default Java ARGB format
		pixelBuffer = convertImagePixelsRGBA(pixels, w, h, false); // convert to bytes in RGBA format
		textureW = GLApp.getPowerOfTwoBiggerThan(w); // the texture size big enough to hold this image
		textureH = GLApp.getPowerOfTwoBiggerThan(h); // the texture size big enough to hold this image
	}

	/**
	 * Save an array of ARGB pixels to a PNG file. If flipY is true, flip the
	 * pixels on the Y axis before saving.
	 */
	public static void savePixelsToPNG(int[] pixels, int width, int height, String imageFilename, boolean flipY)
	{
		if (pixels != null && imageFilename != null)
		{
			if (flipY)
			{
				// flip the pixels vertically (opengl has 0,0 at lower left, java is upper left)
				pixels = GLImage.flipPixels(pixels, width, height);
			}
			try
			{
				// Create a BufferedImage with the RGB pixels then save as PNG
				BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
				image.setRGB(0, 0, width, height, pixels, 0, width);
				javax.imageio.ImageIO.write(image, "png", new File(imageFilename));
			}
			catch (Exception e)
			{
				Message.getInstance().err("GLImage.savePixelsToPNG(" + imageFilename + "): exception " + e);
			}
		}
	}

	//========================================================================
	// Static functions to flip and scale images
	//========================================================================

	/**
	 * Scale the given BufferedImage to width and height that are powers of two.
	 * Return the new scaled BufferedImage.
	 */
	public static BufferedImage convertToPowerOf2(BufferedImage bsrc)
	{
		// find powers of 2 equal to or greater than current dimensions
		int newW = GLApp.getPowerOfTwoBiggerThan(bsrc.getWidth());
		int newH = GLApp.getPowerOfTwoBiggerThan(bsrc.getHeight());
		if (newW == bsrc.getWidth() && newH == bsrc.getHeight())
		{
			return bsrc; // no change necessary
		}
		else
		{
			AffineTransform at = AffineTransform.getScaleInstance((double) newW / bsrc.getWidth(), (double) newH / bsrc.getHeight());
			BufferedImage bdest = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_ARGB);
			Graphics2D g = bdest.createGraphics();
			g.drawRenderedImage(bsrc, at);
			return bdest;
		}
	}

	/**
	 * Scale the given BufferedImage to the given width and height. Return the
	 * new scaled BufferedImage.
	 */
	public static BufferedImage scale(BufferedImage bsrc, int width, int height)
	{
		AffineTransform at = AffineTransform.getScaleInstance((double) width / bsrc.getWidth(), (double) height / bsrc.getHeight());
		BufferedImage bdest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = bdest.createGraphics();
		g.drawRenderedImage(bsrc, at);
		return bdest;
	}

	/**
	 * Flip the given BufferedImage vertically. Return the new flipped
	 * BufferedImage.
	 */
	public static BufferedImage flipY(BufferedImage bsrc)
	{
		AffineTransform tx = AffineTransform.getScaleInstance(1, -1);
		tx.translate(0, -bsrc.getHeight(null));
		AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
		return op.filter(bsrc, null);
	}

}